local modname = minetest.get_current_modname()
-- local modpath = minetest.get_modpath(modname)
local S = minetest.get_translator(modname)

local ingredients_cache = {} -- Store recipe outputs by ingredients
local groups_cache = {}      -- Store items by group
local context = {}            -- Store items by groups

---  Settings
--- ---------------------------------------------------------
local settings = minetest.settings
local opts = {
    show_lazy_toggle = settings:get_bool(modname..'.show_lazy_toggle', false),
    lazy = settings:get_bool(modname..'.lazy', false),
    }

---  Cache helpers
--- ---------------------------------------------------------

--- Recipes by ingedient name
local function create_cache()
    for name, def in pairs(minetest.registered_items) do
        for group, v in pairs(def.groups) do
           if not groups_cache[group] then groups_cache[group] = {} end
           if v > 0 then table.insert(groups_cache[group], name) end
        end
        local recipes = minetest.get_all_craft_recipes(name)
        if recipes then
            for _, recipe in ipairs(recipes) do
                for _, itemstack in ipairs(recipe.items) do
                    local item = ItemStack(itemstack):get_name()
                    if item ~= "" then
                      if not ingredients_cache[item] then
                          ingredients_cache[item] = {}
                      end
                      table.insert(ingredients_cache[item], name)
                    end
                end
            end
        end
    end
end

minetest.register_on_mods_loaded(function()
    create_cache()
end)

-- local function items_for_group(group)
  -- return groups_cache[group] or {}
-- end

local function item_has_groups(item, groups)
 for _, group in ipairs(groups) do
    if minetest.get_item_group(item, group) <= 0  then
      return false
    end
 end
 return true
end

-- local function item_is_groups(item)
 -- return item:sub(1, 6) == "group:"
-- end

local function extract_groups(str)
  if str:sub(1, 6) == "group:" then
    return str:sub(7):split()
  end
  return nil
end
---  Inventory Functions
--- ---------------------------------------------------------

--- Returns true if a least <count> items with groups <groups>
---  are in <listname> of <inv>
local function inventory_contains_groups(inv, listname, groups, count)
    if not count then count = 1 end
    local found = 0
    for  _, itemstack in ipairs(inv:get_list(listname)) do
      if item_has_groups(itemstack:get_name(), groups) then
        found = found + itemstack:get_count()
        if found >= count then return true end
      end
    end
    return false
end

--- Find the item with groups <groups> in <listname> of <inv> that as the highest count
--- using <method>
--- If <all> is true, return a table with stack name as key and count as value end
-- local function inventory_find_group_item(inv, listname, groups, count, avoid, all)
    -- if not count then count = 1 end
    -- if not avoid then avoid = {} end
    -- local max_count = 0
    -- local match
    -- local stacks = {}
    -- for  _, itemstack in ipairs(inv:get_list(listname)) do
      -- local stack_name = itemstack:get_name()
      -- if item_has_groups(stack_name, groups) then
        -- local stack_count = itemstack:get_count()
        -- stacks[stack_name] = stacks[stack_name] or 0 + stack_count
        -- if stacks[stack_name] > max_count then
            -- --- Only update max_count is item is not to be avoided
            -- if not avoid[stack_name] then
              -- match = stack_name
              -- max_count = stacks[stack_name]
            -- --- Otherwise continue and select stack only if there was no other match
            -- elseif not match then
              -- match = stack_name
            -- end
        -- end
      -- end
    -- end
    -- if all then return stacks end
    -- local result = ItemStack(match)
    -- result:set_count(count)
    -- return result
-- end

--- Returns an ItemStack of <count> of first match item with groups <groups> in <listname> of <inv>
--- Try to ignore any true items in <avoid> unless there is no other match
local function inventory_find_group_item_first_match(inv, listname, groups, count, avoid)
  if not ( groups and groups[1] and groups_cache[groups[1]] ) then return end
  --- Find items for first group matching other groups
  local match
  for _, itemname in ipairs(groups_cache[groups[1]]) do
      local stack = ItemStack(itemname..' '..count)
      if item_has_groups(itemname, groups) and inv:contains_item(listname, stack) then
          --- Stop there if first match is not to be avoided
          if not avoid[itemname] then return stack end
          --- Otherwise continue and select stack only if there was no other match
          if not match then match = stack end
      end
  end
  return match
end

--- Returns true if <listname> of <inv> contains all the itemstacks in <items>
local function inventory_has_all_items(inv, listname, items)
    local required_items = {}
    for _, itemstack in pairs(items) do
        itemstack = ItemStack(itemstack)
        local item = itemstack:get_name()
        local count = itemstack:get_count()
        if item ~= "" then
            required_items[item] = ( required_items[item] or 0 ) + count
        end
    end

    for item, count in pairs(required_items) do
        local groups = extract_groups(item)
        if not groups and not inv:contains_item(listname, item..' '..count) then
            return false
        end
        if groups and not inventory_contains_groups(inv, listname, groups, count) then
            return false
        end
    end

    return true
end

--- Returns the first recipe for <result> craftable  with items in <listname> of <inv>
local function get_first_craftable_recipe(inv, listname, result)
  local recipes = minetest.get_all_craft_recipes(result)
  if recipes then
      for _, recipe in ipairs(recipes) do
          if recipe.method == "normal" and inventory_has_all_items(inv, listname, recipe.items) then
              return recipe
          end
      end
  end
end

--- Returns all recipes for <result> craftable with items in <listname> of <inv>
local function get_all_craftable_recipe(inv, listname, result)
  local craftable = {}
  local recipes = minetest.get_all_craft_recipes(result)
  if recipes then
      for _, recipe in ipairs(recipes) do
          if recipe.method == "normal" and inventory_has_all_items(inv, listname, recipe.items) then
             table.insert(craftable, recipe)
          end
      end
  end
  return craftable
end

--- Returns all items that can be crafted at least once with items in <listname> of <inv>
local function get_craftable_items(inv, dtinv_name)
    local g = {}
    local craftable = {}
    local listname = 'main'
    local dtinv = minetest.create_detached_inventory(dtinv_name)
    dtinv:set_size(listname, inv:get_size('main') + inv:get_size('craft'))
    dtinv:set_list(listname, table.insert_all(inv:get_list('main'), inv:get_list('craft')))

    --- Loop through the list of recipes by ingredients
    for name, results in pairs(ingredients_cache) do
        --- Ingredient item with groups can be found in inventory
        local groups = extract_groups(name)
        if ( not groups and dtinv:contains_item(listname, name) )
            or ( groups and inventory_contains_groups(dtinv, listname, groups))
            then
                --- Loop through items that can be made with ingredient
                for _,result in ipairs(results) do
                  if result ~= "" and not g[result] then
                    g[result] = true   --- Store already processed results in g
                    if get_first_craftable_recipe(dtinv, listname, result) then
                        table.insert(craftable, result)  --- Save it
                    end
                  end
                end
        end
    end

    if not minetest.remove_detached_inventory(dtinv_name) then
        minetest.log('warning', '['..modname..']'.. 'Detached inventory could not be removed : '..dump(dtinv_name))
    end
    table.sort(craftable)
    return craftable
end


---  Inventory functions (player and recipes)
--- ---------------------------------------------------------

--- Returns an indexed 9 itemstacks list from a  1x3, 2x3, or 3x3 width recipe
local function recipe_to_craft_invlist(recipe)

  local craft_invlist
  if recipe then
      -- Rewrite recipe to always use 9 slots
      local items = recipe.items
      if recipe.width == 1 then
        craft_invlist = { items[1], nil, nil, items[2], nil, nil, items[3] }
      elseif recipe.width == 2 then
        craft_invlist = { items[1], items[2], nil, items[3], items[4], nil, items[5], items[6] }
      else
        craft_invlist = items
      end
  end
  return craft_invlist
end

--- Fill slot <slot_num> of the craft inventory with item from <craft_recipe>
--- Used by inventory_recipe_stacks_to_craft
local function inventory_recipe_stacks_to_craft_slot(inv, craft_list, slot_num, player_name)
  --- craft_list is a list of 9 itemstacks
  local rc, c = {}
  local i = slot_num
  --- Load context recipe cache
  if player_name then c = context[player_name] end
  if i == 1 or not c.recipe_groupitems then c.recipe_groupitems = {} end
  if not c.recipe_cache then c.recipe_cache = {} end
  if not c.recipe_cache[i] then c.recipe_cache[i] = {} end
  for _,v in ipairs(c.recipe_cache[i]) do rc[v] = true end
  --- Does recipe want something in this slot ?
  if not craft_list[i] then return false end
  -- print("Slot : "..dump(i))

  --- Don't bother if craft slot is full
  local craft_stack = inv:get_stack("craft", i)
  if craft_stack:get_free_space() <= 0 then return false end
  -- print("... has free space")

  local recipe_stack = ItemStack(craft_list[i])
  local craft_stack_name = craft_stack:get_name()
  local recipe_stack_name = recipe_stack:get_name()

  --- Recipe use group
  local groups = extract_groups(recipe_stack_name)
  if groups then
    -- print("... must be of groups ".. dump(groups))

    --- Is there an item in the traget slot ?
    if not craft_stack:is_empty() then
      --- Does it have the required groups ?
      if item_has_groups(craft_stack_name, groups) then
        -- We got a good item, and will try to add more of the same
        recipe_stack:set_name(craft_stack_name)
      --- Otherwise don't bother, our recipe is broken anyway
       else return false
      end
    else --- Empty slot
      --- Let's try to find a matching item
        --- Was there a cached item for this group name ?
      local groupitem_stack
      local groupitem_name = c.recipe_groupitems[recipe_stack_name]
      if groupitem_name then groupitem_stack = ItemStack(groupitem_name..' '..recipe_stack:get_count()) end
      --- Double check that we can use it
      if groupitem_stack and inv:contains_item("main", groupitem_stack) and item_has_groups(groupitem_name, groups)
         then
            recipe_stack = groupitem_stack
      else  --- Everything else failed, look for a new item
            recipe_stack = inventory_find_group_item_first_match(inv, "main", groups, recipe_stack:get_count(), rc)
      end
      --- Cache the result
      if not c.recipe_groupitems[recipe_stack_name] then
        c.recipe_groupitems[recipe_stack_name] = recipe_stack:get_name()
      end
    end
  end
  local selected_stack_name = recipe_stack:get_name()

  --- We want to add a valid stack, be sure it can be added to its slot in craft grid,
  --- and be sure that it can be taken from main inventory
  if recipe_stack:is_empty() or not recipe_stack:is_known() then return false end
  if not craft_stack:item_fits(recipe_stack) then return false end
  if not inv:contains_item("main", recipe_stack) then return false end

  -- Now let's actually do this
  craft_stack:add_item(recipe_stack)
  inv:set_stack("craft", i, craft_stack)
  inv:remove_item("main", recipe_stack)

  if rc[selected_stack_name] and c.recipe_groupitems[recipe_stack_name] == selected_stack_name then
    --- Clear recipe cache if item as been selected even though it was already cached
    if #c.recipe_cache[i] > 1 and ( selected_stack_name ~= craft_stack_name ) then
        c.recipe_cache[i] = {}
    end
  --- Add stack name to context cache for this slot
  else table.insert(c.recipe_cache[i], selected_stack_name)
  end

  --- Update context
  if player_name then context[player_name] = c end
  return true
end

--- Tries to put items evenly from the main inventory into the grid
--- By looping to fill the craft grid with items from recipe
local function inventory_recipe_stacks_to_craft(inv, craft_list, lazy, player_name)

  local is_done = true
  --- Loop trough the 9 solts of the craftgrid
  for i = 1, 9 do
      if inventory_recipe_stacks_to_craft_slot(inv, craft_list, i, player_name) then
        --- if slot could be updated, we're in for another loop
        is_done = false
      else --- Don't bother looking at this slot in the next loop
        craft_list[i] = nil
      end
  end
  --- Do this as much times as needed to fill the craft grid with as much items as possible
  if not ( lazy or is_done ) then
      inventory_recipe_stacks_to_craft(inv, craft_list, lazy, player_name)
  end

  return true
end

--- Fetch recipes for <item> and return the next one based on context
local function recipe_next_for_item(item, inv, player_name)
  local recipe
  --- Load context if possible
  local c = {}
  if player_name and context[player_name] then c = context[player_name] end

  --- Did the player re-click on last item clicked
  if c.output == item and
     c.recipes then
        local index = 1
        local recipes = c.recipes -- fetch cached recipes
        --- Is there more recipes ?
        if #recipes > c.recipe_index then
          index = c.recipe_index + 1
        end
        c.recipe_index = index
        recipe = recipes[index]

  --- The player clicked on new item
  else
        c.recipe_cache = nil -- Clear recipe cache
        c.output = item
        local recipes = get_all_craftable_recipe(inv, "main", item)
        if #recipes <= 0 then return end
        c.recipes = recipes
        c.recipe_index = 1
        recipe = recipes[1]
  end
  --- Update context if possible
  if player_name then context[player_name] = c end
  return recipe
end

--- Move all items from craft grid to main inventory
local function inventory_clear_craft_grid(inv, to_list)
    for i = 1, 9 do
        local stack = inv:get_stack("craft", i)
        if not stack:is_empty() then
            if inv:room_for_item('main', stack) then
              inv:add_item("main", stack)
              inv:set_stack("craft", i, ItemStack(nil))
            else
              return false
            end
        end
    end
    return true
end


---  Formspec Functions
--- ---------------------------------------------------------

-- Generate the recipe with craftable recipes
local function formspec_get_recipes_menu(player_name, x_index, y_index)
    if not x_index then x_index = 0 end
    if not y_index then y_index = 0 end
    local x = x_index
    local y = y_index
    local c = {}
    if context and player_name and context[player_name] then c = context[player_name] end
    local n = c.page_num or 1

    local player = minetest.get_player_by_name(player_name)
    if not player then
        return ""
    end

    local inv = player:get_inventory()

    if not c.craftable_items then c.craftable_items = get_craftable_items(inv, modname..'_'..player_name) end
    local craftable_items = c.craftable_items
    local max_pages = math.ceil(#craftable_items / ( 4 * 9 ))

    if n < 1 then n = max_pages end
    if n > max_pages then n = 1 end
    local istart = 1 + (( n - 1 ) * 4 * 8)
    if istart >= #craftable_items then istart = 1 ; n = 1 end
    local iend = (( n ) * 4 * 9)

    local recipes_menu = "label[".. x ..","..y..";"..S('Craftable Recipes :').."]"
    local step = 0.95
    y = y + 0.5
    local y_max = y_index + ( 8 * step )
    local x_max = x_index + ( 3 * step )

    -- for _, recipe in ipairs(craftable_items) do
    for i = istart, iend do
        if craftable_items[i] then
          local recipe = craftable_items[i]
          recipes_menu = recipes_menu
            .. "item_image_button[".. x .."," .. y .. ";1,1;" .. recipe .. ";" .. recipe .. ";" .. '' .. "]"
          x = x + step
          if x > x_max then y = y + step ; x = x_index ; end
          if y > y_max then break end
        end
    end

    if n > 1 or #craftable_items > iend then
      recipes_menu = recipes_menu
          .. "image_button["..x_index..","..y_max + 0.8 ..";0.8,0.8;crafting_table_icon_prev.png;prev;;false;false;]"
      recipes_menu = recipes_menu
          .. "image_button["..x_max  + 0.2 ..",".. y_max + 0.8 ..";0.8,0.8;crafting_table_icon_next.png;next;;false;false;]"
      recipes_menu = recipes_menu
          .. "image_button["..x_index +( 2 * step )..","..y_max + 0.8 ..";0.8,0.8;;update;".. n .."/"..max_pages.. ";false;false;]"
    end

    recipes_menu = recipes_menu
          .. "image_button["..x_index + ( 1 * step )..","..y_max + 0.8 ..";0.8,0.8;crafting_table_icon_refresh.png;update;;false;false;]"

    c.page_num = n
    context[player_name] = c
    return recipes_menu
end

--- Create the formspec for player
local function get_formspec(player_name)
    local additional_menu = formspec_get_recipes_menu(player_name, 8.1)
    local crafting_grid = "list[current_player;craft;1.75,1;3,3;]" ..
                          "listring[current_player;craft]"
    local crafting_output = "list[current_player;craftpreview;5.75,1.75;1,1;]"
    local main_inventory = "list[current_player;main;0,5.2;8,4;]" ..
                            "listring[current_player;main]"
    local clear_button = "image_button[1,3.3;0.8,0.8;crafting_table_icon_clear.png;clear;;false;false;]"
    local formspec = "size[12,9.1]" ..
                     crafting_grid ..
                     crafting_output ..
                     main_inventory ..
                     clear_button ..
                     additional_menu

    if context and context[player_name] then
      local c = context[player_name]
      if c.error_message then
        formspec = formspec .. "label[0.3,0;"..minetest.colorize('red', c.error_message).."]"
      end
      if opts.show_lazy_toggle then
        formspec = formspec .. "checkbox[6,0.3;lazy;"..S('Lazy')..";".. tostring(c.lazy or opts.lazy ) .."]"
      end
    end
    return formspec
end

--- Show the formspec to the player
local function show_formspec(player_name)
    if not context[player_name] then context[player_name] = {} end
    local formspec = get_formspec(player_name)
    minetest.show_formspec(player_name, modname..":crafting_table_formspec", formspec)
end

---  Node Registration
--- ---------------------------------------------------------
local crafting_table = modname..":crafting_table"

minetest.register_node(crafting_table, {
    description = "Crafting Table",
    tiles = {
          "crafting_table_top.png",
          "default_wood.png",
          "crafting_table_side.png",
          "crafting_table_side.png",
          "crafting_table_side.png",
          "crafting_table_front.png"
        },
    groups = {choppy=2,oddly_breakable_by_hand=2},
    is_ground_content = false,
    sounds = default.node_sound_wood_defaults(),
    on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
        local player_name = clicker:get_player_name()
        if player_name then
            show_formspec(player_name)
        end
    end,
})

--- Register recipes
minetest.register_craft({
  output = crafting_table,
  recipe = {
    {'group:wood', 'group:wood'},
    {'group:tree', 'group:tree'},
  }
})

if minetest.registered_nodes["craft_table:simple"] then
    minetest.register_craft({
      output = crafting_table,
      recipe = { {'craft_table:simple'}, {'group:tree'} }
    })
end

if minetest.registered_nodes["xdecor:workbench"] then
    minetest.register_craft({
      output = crafting_table,
      recipe = { {'xdecor:workbench'}, {'group:tree'} }
    })
end

if minetest.registered_nodes["crafting_bench:workbench"] then
    minetest.register_craft({
      output = 'crafting_bench:workbench',
      recipe = {
          {'default:steel_ingot','default:steel_ingot'},
          {'default:steel_ingot', crafting_table}
          }
    })
end

---  Form processing
--- ---------------------------------------------------------

--- Handle button clicks
minetest.register_on_player_receive_fields(function(player, formname, fields)
    -- print(dump(context))
    local player_name = player:get_player_name()
    if formname == modname..":crafting_table_formspec" then
        -- print(dump(fields))
        --- Stop there if we're just exiting
        if fields.quit or fields.key_enter then
          context[player_name] = nil  -- Clear context
          return
        end

        --- Load context and inventory
        local inv = player:get_inventory()
        if not player_name then return end
        if not context[player_name] then context[player_name] = {} end
        local c = context[player_name]
        c.error_message = nil

        --- Do we just want to change the lazy option ?
        if fields.lazy then
          if fields.lazy == 'true' then c.lazy = true else c.lazy = false end
          return
        end

        --- Do we just want to switch page ?
        local page_num
        if fields.prev then
           page_num = c.page_num or 2
           c.page_num = page_num - 1
           show_formspec(player_name)
           return true
        elseif fields.next then
           page_num = c.page_num or 1
           c.page_num = page_num + 1
           show_formspec(player_name)
           return true
        end

        --- Force reset the list of craftable items if update was requested
        if fields.update then
            c.craftable_items = nil
        --- For anything else than page switch and update we are clearing the craft grid
        elseif not inventory_clear_craft_grid(inv, 'main') then
            --- Complain if this failed
            c.error_message = S("Unable to clear the crafting grid beacause inventory is full !")
        end

        -- Update the formspec before moving things out of main inventory
        if player_name then show_formspec(player_name) end

        --- Stop there if we failed to clear the craft grid
        if not inv:is_empty('craft') then return end

        --- Stop there if we were just clearing the craft inventory and/or updating
        if fields.clear then c.recipe_cache = nil ; return end
        if fields.update then return end

        for field, v in pairs(fields) do
            local recipe_output

            --- Is it the recipe output ?
            if minetest.registered_items[field] then
              recipe_output = field
            --- Stop there if we're trying to fetch recipe for an unknown item
            else return
            end

            --- Stop there if no reciped output
            if not recipe_output then return end

            --- Fetch the recipe we're looking for
            local recipe = recipe_next_for_item(recipe_output, inv, player_name)

            --- Format the recipe as a list of 9 itemstacks compatible with 3x3 grid
            local craft_list = recipe_to_craft_invlist(table.copy(recipe))
            if not craft_list then return end

            -- Add stacks from the main inventory to the craft grid
            inventory_recipe_stacks_to_craft(inv, craft_list, c.lazy, player_name)

            -- Update the formspec
            -- if player_name then show_formspec(player_name) end
            -- return true
        end
    else context[player_name] = nil
    end
end)
